﻿namespace ImapComponent.Net.Mail.Utils
{
    using System;
    using System.IO;
    using System.Net.Mime;
    using System.Text;
    using System.Text.RegularExpressions;

    /// <summary>
    /// Encodes and decodes string.
    /// </summary>
    internal class StringEncoder
    {
        #region Methods

        #region Encoding and Decoding Methods

        /// <summary>
        /// Decodes string from UTF7.
        /// </summary>
        /// <param name="str">A string to be decoded from UTF7.</param>
        /// <returns>An UTF7 decoded string.</returns>
        internal static string DecodeUtf7(string str)
        {
            str = str.Replace("&", "+");
            return Encoding.UTF7.GetString(Encoding.ASCII.GetBytes(str));
        }

        /// <summary>
        /// Encode string to UTF7.
        /// </summary>
        /// <param name="str">A string to be encoded to UTF7.</param>
        /// <returns>An UTF7 encoded string.</returns>
        internal static string EncodeUtf7(string str)
        {
            //return Encoding.Default.GetString(Encoding.UTF7.GetBytes(str)).Replace("+", "&");
            return str;
        }

        /// <summary>
        /// Decodes the header value.
        /// </summary>
        /// <param name="str">A string to be decoded.</param>
        /// <returns>A decoded value.</returns>
        internal static string DecodeHeaderValue(string str)
        {
            string result = string.Empty;

            int offset = 0;

            while (offset < str.Length)
            {
                int start = str.IndexOf("=?", offset);
                int end = str.IndexOf("?=", start + 2);

                if (start > -1 && end > -1)
                {
                    if (start - offset > 0)
                    {
                        result += str.Substring(offset, start - offset);
                    }

                    while (true)
                    {
                        string stringToDecode = str.Substring(start + 2, end - start - 2);
                        string[] decodePart = stringToDecode.Split(new char[] { '?' });

                        if (decodePart.Length == 3)
                        {
                            string decodedString;

                            if (decodePart[1].ToUpper() == "B")
                            {
                                decodedString = Encoding.GetEncoding(decodePart[0]).GetString(Convert.FromBase64String(decodePart[2]));
                            }
                            else if (decodePart[1].ToUpper() == "Q")
                            {
                                decodedString = Encoding.GetEncoding(decodePart[0]).GetString(DecodeQuotedPrintable(Encoding.ASCII.GetBytes(decodePart[2].Replace("_", " "))));
                            }
                            else
                            {
                                decodedString = stringToDecode;
                            }

                            result += decodedString;
                            offset = end + 2;
                            break;
                        }
                        else if (decodePart.Length < 3)
                        {
                            end = str.IndexOf("?=", end + 2);

                            if (end == -1)
                            {
                                result += "=?";
                                offset = start + 2;
                                break;
                            }
                        }
                        else
                        {
                            result += "=?";
                            offset = start + 2;
                            break;
                        }
                    }
                }
                else
                {
                    if (str.Length > offset)
                    {
                        result += str.Substring(offset);
                        offset = str.Length;
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Decodes a quoted printable array.
        /// </summary>
        /// <param name="quotedBytes">A quoted printable array.</param>
        /// <returns>A decoded byte array.</returns>
        private static byte[] DecodeQuotedPrintable(byte[] quotedBytes)
        {
            MemoryStream inputStream = new MemoryStream(quotedBytes);
            MemoryStream resultStream = new MemoryStream();

            int i = inputStream.ReadByte();

            while (i > -1)
            {
                if (i == '=')
                {
                    byte[] buffer = new byte[2];
                    inputStream.Read(buffer, 0, 2);

                    if (!(buffer[0] == '\r' && buffer[1] == '\n'))
                    {
                        try
                        {
                            byte[] convertedByte = DecodeHex(buffer);
                            resultStream.Write(convertedByte, 0, convertedByte.Length);
                        }
                        catch
                        {
                            // skip invalid hex characters
                        }
                    }
                }
                else
                {
                    resultStream.WriteByte((byte)i);
                }

                i = inputStream.ReadByte();
            }

            return resultStream.ToArray();
        }

        /// <summary>
        /// Decodes a hex byte array.
        /// </summary>
        /// <param name="hexBytes">A hex byte array.</param>
        /// <returns>A decoded byte array.</returns>
        private static byte[] DecodeHex(byte[] hexBytes)
        {
            if (hexBytes.Length % 2 == 1)
            {
                throw new ArgumentException("Invalid byte array.");
            }

            byte[] decodedArray = new byte[hexBytes.Length / 2];

            for (int i = 0; i < hexBytes.Length; i += 2)
            {
                byte[] hexPairInDecimal = new byte[2];

                for (int j = 0; j < 2; j++)
                {
                    string hex = ((char)hexBytes[i + j]).ToString().ToUpper();

                    switch (hex)
                    {
                        case "0":
                            hexPairInDecimal[j] = 0;
                            break;
                        case "1":
                            hexPairInDecimal[j] = 1;
                            break;
                        case "2":
                            hexPairInDecimal[j] = 2;
                            break;
                        case "3":
                            hexPairInDecimal[j] = 3;
                            break;
                        case "4":
                            hexPairInDecimal[j] = 4;
                            break;
                        case "5":
                            hexPairInDecimal[j] = 5;
                            break;
                        case "6":
                            hexPairInDecimal[j] = 6;
                            break;
                        case "7":
                            hexPairInDecimal[j] = 7;
                            break;
                        case "8":
                            hexPairInDecimal[j] = 8;
                            break;
                        case "9":
                            hexPairInDecimal[j] = 9;
                            break;
                        case "A":
                            hexPairInDecimal[j] = 10;
                            break;
                        case "B":
                            hexPairInDecimal[j] = 11;
                            break;
                        case "C":
                            hexPairInDecimal[j] = 12;
                            break;
                        case "D":
                            hexPairInDecimal[j] = 13;
                            break;
                        case "E":
                            hexPairInDecimal[j] = 14;
                            break;
                        case "F":
                            hexPairInDecimal[j] = 15;
                            break;
                        default:
                            throw new ArgumentException("Invalid byte data.");
                    }
                }

                decodedArray[i / 2] = (byte)((hexPairInDecimal[0] << 4) | hexPairInDecimal[1]);
            }

            return decodedArray;
        }

        /// <summary>
        /// Decodes the data from base64.
        /// </summary>
        /// <param name="base64Bytes">The base64 data.</param>
        /// <returns>A decoded byte array.</returns>
        private static byte[] DecodeBase64(byte[] base64Bytes)
        {
            string stringData = Encoding.ASCII.GetString(base64Bytes);

            return Convert.FromBase64String(stringData);
        }

        /// <summary>
        /// Decodes the data.
        /// </summary>
        /// <param name="transferEncoding">A transfer encoding.</param>
        /// <param name="data">A byte array data.</param>
        /// <returns>A decoded byte array.</returns>
        internal static byte[] DecodeData(TransferEncoding transferEncoding, byte[] data)
        {
            switch (transferEncoding)
            {
                case TransferEncoding.Base64:
                    return DecodeBase64(data);
                case TransferEncoding.QuotedPrintable:
                    return DecodeQuotedPrintable(data);
                case TransferEncoding.SevenBit:
                    return Encoding.ASCII.GetBytes(Encoding.ASCII.GetString(data));
                case TransferEncoding.Unknown:
                    return data;
                default:
                    return data;
            }
        }

        /// <summary>
        /// Decodes the string.
        /// </summary>
        /// <param name="charset">A charset.</param>
        /// <param name="transferEncoding">A transfer encoding.</param>
        /// <param name="data">A byte array data.</param>
        /// <returns>A decoded string.</returns>
        internal static string DecodeString(string charset, TransferEncoding transferEncoding, byte[] data)
        {
            switch (transferEncoding)
            {
                case TransferEncoding.Base64:
                    if (string.IsNullOrEmpty(charset))
                    {
                        return Encoding.Default.GetString(DecodeBase64(data));
                    }
                    else
                    {
                        return Encoding.GetEncoding(charset).GetString(DecodeBase64(data));
                    }
                case TransferEncoding.QuotedPrintable:
                    if (string.IsNullOrEmpty(charset))
                    {
                        return Encoding.Default.GetString(DecodeQuotedPrintable(data));
                    }
                    else
                    {
                        return Encoding.GetEncoding(charset).GetString(DecodeQuotedPrintable(data));
                    }
                case TransferEncoding.SevenBit:
                    if (string.IsNullOrEmpty(charset))
                    {
                        return Encoding.Default.GetString(Encoding.ASCII.GetBytes(Encoding.ASCII.GetString(data)));
                    }
                    else
                    {
                        return Encoding.GetEncoding(charset).GetString(Encoding.ASCII.GetBytes(Encoding.ASCII.GetString(data)));
                    }
                case TransferEncoding.Unknown:
                    if (string.IsNullOrEmpty(charset))
                    {
                        return Encoding.Default.GetString(data);
                    }
                    else
                    {
                        return Encoding.GetEncoding(charset).GetString(data);
                    }
                default:
                    if (string.IsNullOrEmpty(charset))
                    {
                        return Encoding.Default.GetString(data);
                    }
                    else
                    {
                        return Encoding.GetEncoding(charset).GetString(data);
                    }
            }
        }

        /// <summary>
        /// Gets the value indicating if the string needs to be decoded or not.
        /// </summary>
        /// <param name="data">A string data.</param>
        /// <returns>true if the string is encoded; otherwise, false</returns>
        internal static bool IsEncoded(string data)
        {
            return Regex.IsMatch(data, @"^=?[^\?]+\?[^\?]+\?.*\?=$");
        }

        #endregion // Encoding and Decoding Methods

        #endregion // Methods
    }
}
